Passed
Pull Request — master (#127)
by
unknown
01:42
created

index.js ➔ readSync   F

Complexity

Conditions 21

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 0
c 0
b 0
f 0
cc 21

How to fix   Complexity   

Complexity

Complex classes like index.js ➔ readSync often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
const fs = require('fs')
2
const ID3Definitions = require("./src/ID3Definitions")
3
const ID3Frames = require('./src/ID3Frames')
4
const ID3Util = require('./src/ID3Util')
5
const zlib = require('zlib')
6
const { isFunction, isString } = require('./src/util')
7
8
/*
9
**  Used specification: http://id3.org/id3v2.3.0
10
*/
11
12
function writeInBuffer(tags, buffer, fn) {
13
    buffer = removeTagsFromBuffer(buffer) || buffer
14
    const completeBuffer = Buffer.concat([tags, buffer])
15
    if(isFunction(fn)) {
16
        fn(null, completeBuffer)
17
        return undefined
18
    }
19
    return completeBuffer
20
}
21
22
function writeAsync(tags, filename, fn) {
23
    try {
24
        fs.readFile(filename, function(error, data) {
25
            if(error) {
26
                fn(error)
27
                return
28
            }
29
            data = removeTagsFromBuffer(data) || data
30
            const newData = Buffer.concat([tags, data])
31
            fs.writeFile(filename, newData, 'binary', (err) => {
32
                fn(err)
33
            })
34
        }.bind(this))
0 ignored issues
show
unused-code introduced by
The call to bind does not seem necessary since the function does not use this. Consider calling it directly.
Loading history...
35
    } catch(err) {
36
        fn(err)
37
    }
38
}
39
40
function writeSync(tags, filename) {
41
    try {
42
        let data = fs.readFileSync(filename)
43
        data = removeTagsFromBuffer(data) || data
44
        const newData = Buffer.concat([tags, data])
45
        fs.writeFileSync(filename, newData, 'binary')
46
    } catch(error) {
47
        return error
48
    }
49
    return true
50
}
51
52
/**
53
 * Write passed tags to a file/buffer
54
 * @param tags - Object containing tags to be written
55
 * @param filebuffer - Can contain a filepath string or buffer
56
 * @param fn - (optional) Function for async version
57
 * @returns {boolean|Buffer|Error}
58
 */
59
module.exports.write = function(tags, filebuffer, fn) {
60
    const completeTags = this.create(tags)
61
62
    if(filebuffer instanceof Buffer) {
63
        return writeInBuffer(completeTags, filebuffer, fn)
64
    }
65
    if(isFunction(fn)) {
66
        return writeAsync(completeTags, filebuffer, fn)
67
    }
68
    return writeSync(completeTags, filebuffer)
69
}
70
71
/**
72
 * Creates a buffer containing the ID3 Tag
73
 * @param tags - Object containing tags to be written
74
 * @param fn fn - (optional) Function for async version
75
 * @returns {Buffer}
76
 */
77
module.exports.create = function(tags, fn) {
78
    let frames = []
79
80
    //  Create & push a header for the ID3-Frame
81
    const header = Buffer.alloc(10)
82
    header.fill(0)
83
    header.write("ID3", 0)              //File identifier
84
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
85
    header.writeUInt16BE(0x0000, 5)     //Flags 00
86
87
    //Last 4 bytes are used for header size, but have to be inserted later, because at this point, its size is not clear.
88
    frames.push(header)
89
90
    frames = frames.concat(this.createBuffersFromTags(tags))
91
92
    //  Calculate frame size of ID3 body to insert into header
93
94
    let totalSize = 0
95
    frames.forEach((frame) => {
96
        totalSize += frame.length
97
    })
98
99
    //  Don't count ID3 header itself
100
    totalSize -= 10
101
    //  ID3 header size uses only 7 bits of a byte, bit shift is needed
102
    let size = ID3Util.encodeSize(totalSize)
103
104
    //  Write bytes to ID3 frame header, which is the first frame
105
    frames[0].writeUInt8(size[0], 6)
106
    frames[0].writeUInt8(size[1], 7)
107
    frames[0].writeUInt8(size[2], 8)
108
    frames[0].writeUInt8(size[3], 9)
109
110
    if(isFunction(fn)) {
111
        fn(Buffer.concat(frames))
112
    } else {
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
113
        return Buffer.concat(frames)
114
    }
115
}
116
117
/**
118
 * Returns array of buffers created by tags specified in the tags argument
119
 * @param tags - Object containing tags to be written
120
 * @returns {Array}
121
 */
122
module.exports.createBuffersFromTags = function(tags) {
123
    let frames = []
124
    if(!tags) {
125
        return frames
126
    }
127
    const rawObject = Object.keys(tags).reduce((acc, val) => {
128
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
129
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
130
        } else if(ID3Definitions.FRAME_IDENTIFIERS.v4[val] !== undefined) {
131
            /**
132
             * Currently, node-id3 always writes ID3 version 3.
133
             * However, version 3 and 4 are very similar, and node-id3 can also read version 4 frames.
134
             * Until version 4 is fully supported, as a workaround, allow writing version 4 frames into a version 3 tag.
135
             * If a reader does not support a v4 frame, it's (per spec) supposed to skip it, so it should not be a problem.
136
             */
137
            acc[ID3Definitions.FRAME_IDENTIFIERS.v4[val]] = tags[val]
138
        } else {
139
            acc[val] = tags[val]
140
        }
141
        return acc
142
    }, {})
143
144
    Object.keys(rawObject).forEach((specName) => {
145
        let frame
146
        // Check if invalid specName
147
        if(specName.length !== 4) {
148
            return
149
        }
150
        if(ID3Frames[specName] !== undefined) {
151
            frame = ID3Frames[specName].create(rawObject[specName], 3, this)
152
        } else if(specName.startsWith('T')) {
153
            frame = ID3Frames.GENERIC_TEXT.create(specName, rawObject[specName], 3)
154
        } else if(specName.startsWith('W')) {
155
            if(ID3Util.getSpecOptions(specName, 3).multiple && rawObject[specName] instanceof Array && rawObject[specName].length > 0) {
156
                frame = Buffer.alloc(0)
157
                // deduplicate array
158
                for(let url of [...new Set(rawObject[specName])]) {
159
                    frame = Buffer.concat([frame, ID3Frames.GENERIC_URL.create(specName, url, 3)])
160
                }
161
            } else {
162
                frame = ID3Frames.GENERIC_URL.create(specName, rawObject[specName], 3)
163
            }
164
        }
165
166
        if (frame && frame instanceof Buffer) {
167
            frames.push(frame)
168
        }
169
    })
170
171
    return frames
172
}
173
174
function readSync(filebuffer, options) {
175
    if(isString(filebuffer)) {
176
        filebuffer = fs.readFileSync(filebuffer)
177
    }
178
    return this.getTagsFromBuffer(filebuffer, options)
179
}
180
181
function readAsync(filebuffer, options, fn) {
182
    if(isString(filebuffer)) {
183
        fs.readFile(filebuffer, (error, data) => {
184
            if(error) {
185
                fn(error, null)
186
            } else {
187
                fn(null, this.getTagsFromBuffer(data, options))
188
            }
189
        })
190
    } else {
191
        fn(null, this.getTagsFromBuffer(filebuffer, options))
192
    }
193
}
194
195
/**
196
 * Read ID3-Tags from passed buffer/filepath
197
 * @param filebuffer - Can contain a filepath string or buffer
198
 * @param options - (optional) Object containing options
199
 * @param fn - (optional) Function for async version
200
 * @returns {boolean}
201
 */
202
module.exports.read = function(filebuffer, options, fn) {
203
    if(!options || typeof options === 'function') {
204
        fn = fn || options
205
        options = {}
206
    }
207
    if(isFunction(fn)) {
208
        return readAsync.bind(this)(filebuffer, options, fn)
209
    }
210
    return readSync.bind(this)(filebuffer, options)
211
}
212
213
/**
214
 * Update ID3-Tags from passed buffer/filepath
215
 * @param tags - Object containing tags to be written
216
 * @param filebuffer - Can contain a filepath string or buffer
217
 * @param options - (optional) Object containing options
218
 * @param fn - (optional) Function for async version
219
 * @returns {boolean|Buffer|Error}
220
 */
221
module.exports.update = function(tags, filebuffer, options, fn) {
222
    if(!options || typeof options === 'function') {
223
        fn = fn || options
224
        options = {}
225
    }
226
227
    const rawTags = Object.keys(tags).reduce((acc, val) => {
228
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
229
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
230
        } else {
231
            acc[val] = tags[val]
232
        }
233
        return acc
234
    }, {})
235
236
    const updateFn = (currentTags) => {
237
        currentTags = currentTags.raw || {}
238
        Object.keys(rawTags).map((specName) => {
239
            const options = ID3Util.getSpecOptions(specName, 3)
240
            const cCompare = {}
241
            if(options.multiple && currentTags[specName] && rawTags[specName]) {
242
                if(options.updateCompareKey) {
243
                    currentTags[specName].forEach((cTag, index) => {
244
                        cCompare[cTag[options.updateCompareKey]] = index
245
                    })
246
247
                }
248
                if (!(rawTags[specName] instanceof Array)) {
249
                    rawTags[specName] = [rawTags[specName]]
250
                }
251
                rawTags[specName].forEach((rTag) => {
252
                    const comparison = cCompare[rTag[options.updateCompareKey]]
253
                    if (comparison !== undefined) {
254
                        currentTags[specName][comparison] = rTag
255
                    } else {
256
                        currentTags[specName].push(rTag)
257
                    }
258
                })
259
            } else {
260
                currentTags[specName] = rawTags[specName]
261
            }
262
        })
263
264
        return currentTags
265
    }
266
267
    if(!isFunction(fn)) {
268
        return this.write(updateFn(this.read(filebuffer, options)), filebuffer)
269
    }
270
271
    this.write(updateFn(this.read(filebuffer, options)), filebuffer, fn)
272
}
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
273
274
module.exports.getTagsFromBuffer = function(filebuffer, options) {
275
    let framePosition = ID3Util.getFramePosition(filebuffer)
276
    if(framePosition === -1) {
277
        return this.getTagsFromFrames([], 3, options)
278
    }
279
    const frameSize = ID3Util.decodeSize(filebuffer.slice(framePosition + 6, framePosition + 10)) + 10
280
    let ID3Frame = Buffer.alloc(frameSize + 1)
281
    filebuffer.copy(ID3Frame, 0, framePosition)
282
    //ID3 version e.g. 3 if ID3v2.3.0
283
    let ID3Version = ID3Frame[3]
284
    const tagFlags = ID3Util.parseTagHeaderFlags(ID3Frame)
285
    let extendedHeaderOffset = 0
286
    if(tagFlags.extendedHeader) {
287
        if(ID3Version === 3) {
288
            extendedHeaderOffset = 4 + filebuffer.readUInt32BE(10)
289
        } else if(ID3Version === 4) {
290
            extendedHeaderOffset = ID3Util.decodeSize(filebuffer.slice(10, 14))
291
        }
292
    }
293
    let ID3FrameBody = Buffer.alloc(frameSize - 10 - extendedHeaderOffset)
294
    filebuffer.copy(ID3FrameBody, 0, framePosition + 10 + extendedHeaderOffset)
295
296
    let frames = this.getFramesFromID3Body(ID3FrameBody, ID3Version, options)
297
298
    return this.getTagsFromFrames(frames, ID3Version, options)
299
}
300
301
module.exports.getFramesFromID3Body = function(ID3FrameBody, ID3Version, options = {}) {
302
    let currentPosition = 0
303
    let frames = []
304
    if(!ID3FrameBody || !(ID3FrameBody instanceof Buffer)) {
305
        return frames
306
    }
307
308
    let identifierSize = 4
309
    let textframeHeaderSize = 10
310
    if(ID3Version === 2) {
311
        identifierSize = 3
312
        textframeHeaderSize = 6
313
    }
314
315
    while(currentPosition < ID3FrameBody.length && ID3FrameBody[currentPosition] !== 0x00) {
316
        let bodyFrameHeader = Buffer.alloc(textframeHeaderSize)
317
        ID3FrameBody.copy(bodyFrameHeader, 0, currentPosition)
318
319
        let decodeSize = false
320
        if(ID3Version === 4) {
321
            decodeSize = true
322
        }
323
        let bodyFrameSize = ID3Util.getFrameSize(bodyFrameHeader, decodeSize, ID3Version)
324
        if(bodyFrameSize + 10 > (ID3FrameBody.length - currentPosition)) {
325
            break
326
        }
327
        const specName = bodyFrameHeader.toString('utf8', 0, identifierSize)
328
        if(options.exclude instanceof Array && options.exclude.includes(specName) || options.include instanceof Array && !options.include.includes(specName)) {
329
            currentPosition += bodyFrameSize + textframeHeaderSize
330
            continue
331
        }
332
        const frameHeaderFlags = ID3Util.parseFrameHeaderFlags(bodyFrameHeader, ID3Version)
333
        let bodyFrameBuffer = Buffer.alloc(bodyFrameSize)
334
        ID3FrameBody.copy(bodyFrameBuffer, 0, currentPosition + textframeHeaderSize + (frameHeaderFlags.dataLengthIndicator ? 4 : 0))
335
        //  Size of sub frame + its header
336
        currentPosition += bodyFrameSize + textframeHeaderSize
337
        frames.push({
338
            name: specName,
339
            flags: frameHeaderFlags,
340
            body: frameHeaderFlags.unsynchronisation ? ID3Util.processUnsynchronisedBuffer(bodyFrameBuffer) : bodyFrameBuffer
341
        })
342
    }
343
344
    return frames
345
}
346
347
module.exports.getTagsFromFrames = function(frames, ID3Version, options = {}) {
348
    let tags = { }
349
    let raw = { }
350
351
    frames.forEach((frame) => {
352
        let specName
353
        let identifier
354
        if(ID3Version === 2) {
355
            specName = ID3Definitions.FRAME_IDENTIFIERS.v3[ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]]
356
            identifier = ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]
357
        } else if(ID3Version === 3 || ID3Version === 4) {
358
            /**
359
             * Due to their similarity, it's possible to mix v3 and v4 frames even if they don't exist in their corrosponding spec.
360
             * Programs like Mp3tag allow you to do so, so we should allow reading e.g. v4 frames from a v3 ID3 Tag
361
             */
362
            specName = frame.name
363
            identifier = ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v3[frame.name] || ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v4[frame.name]
364
        }
365
366
        if(!specName || !identifier || frame.flags.encryption) {
367
            return
368
        }
369
370
        if(frame.flags.compression) {
371
            if(frame.body.length < 5) {
372
                return
373
            }
374
            const inflatedSize = frame.body.readInt32BE()
375
            /*
376
            * ID3 spec defines that compression is stored in ZLIB format, but doesn't specify if header is present or not.
377
            * ZLIB has a 2-byte header.
378
            * 1. try if header + body decompression
379
            * 2. else try if header is not stored (assume that all content is deflated "body")
380
            * 3. else try if inflation works if the header is omitted (implementation dependent)
381
            * */
382
            try {
383
                frame.body = zlib.inflateSync(frame.body.slice(4))
384
            } catch (e) {
385
                try {
386
                    frame.body = zlib.inflateRawSync(frame.body.slice(4))
387
                } catch (e) {
388
                    try {
389
                        frame.body = zlib.inflateRawSync(frame.body.slice(6))
390
                    } catch (e) {
391
                        return
392
                    }
393
                }
394
            }
395
            if(frame.body.length !== inflatedSize) {
396
                return
397
            }
398
        }
399
400
        let decoded
401
        if(ID3Frames[specName]) {
402
            decoded = ID3Frames[specName].read(frame.body, ID3Version, this)
403
        } else if(specName.startsWith('T')) {
404
            decoded = ID3Frames.GENERIC_TEXT.read(frame.body, ID3Version)
405
        } else if(specName.startsWith('W')) {
406
            decoded = ID3Frames.GENERIC_URL.read(frame.body, ID3Version)
407
        }
408
409
        if(decoded) {
410
            if(ID3Util.getSpecOptions(specName, ID3Version).multiple) {
411
                if(!options.onlyRaw) {
412
                    if(!tags[identifier]) {
413
                        tags[identifier] = []
414
                    }
415
                    tags[identifier].push(decoded)
416
                }
417
                if(!options.noRaw) {
418
                    if(!raw[specName]) {
419
                        raw[specName] = []
420
                    }
421
                    raw[specName].push(decoded)
422
                }
423
            } else {
424
                if(!options.onlyRaw) {
425
                    tags[identifier] = decoded
426
                }
427
                if(!options.noRaw) {
428
                    raw[specName] = decoded
429
                }
430
            }
431
        }
432
    })
433
434
    if(options.onlyRaw) {
435
        return raw
436
    }
437
    if(options.noRaw) {
438
        return tags
439
    }
440
441
    tags.raw = raw
442
    return tags
443
}
444
445
/**
446
 * Checks and removes already written ID3-Frames from a buffer
447
 * @param data - Buffer
448
 * @returns {boolean|Buffer}
449
 */
450
module.exports.removeTagsFromBuffer = removeTagsFromBuffer
451
function removeTagsFromBuffer(data) {
452
    let framePosition = ID3Util.getFramePosition(data)
453
454
    if (framePosition === -1) {
455
        return data
456
    }
457
458
    let hSize = Buffer.from([data[framePosition + 6], data[framePosition + 7], data[framePosition + 8], data[framePosition + 9]])
459
460
    const isMsbSet = !!((hSize[0] | hSize[1] | hSize[2] | hSize[3]) & 0x80)
461
    if (isMsbSet) {
462
        //  Invalid tag size (msb not 0)
463
        return false
464
    }
465
466
    if (data.length >= framePosition + 10) {
467
        const size = ID3Util.decodeSize(data.slice(framePosition + 6, framePosition + 10))
468
        return Buffer.concat([data.slice(0, framePosition), data.slice(framePosition + size + 10)])
469
    }
470
471
    return data
472
}
473
474
/**
475
 * @param {string} filepath - Filepath to file
476
 * @returns {boolean|Error}
477
 */
478
function removeTagsSync(filepath) {
479
    let data
480
    try {
481
        data = fs.readFileSync(filepath)
482
    } catch(error) {
483
        return error
484
    }
485
486
    const newData = removeTagsFromBuffer(data)
487
    if(!newData) {
488
        return false
489
    }
490
491
    try {
492
        fs.writeFileSync(filepath, newData, 'binary')
493
    } catch(error) {
494
        return error
495
    }
496
497
    return true
498
}
499
500
/**
501
 * @param {string} filepath - Filepath to file
502
 * @param {(error: Error) => void} fn - Function for async usage
503
 * @returns {void}
504
 */
505
function removeTagsAsync(filepath, fn) {
506
    fs.readFile(filepath, (error, data) => {
507
        if(error) {
508
            fn(error)
509
        }
510
511
        const newData = removeTagsFromBuffer(data)
512
        if(!newData) {
513
            fn(error)
514
            return
515
        }
516
517
        fs.writeFile(filepath, newData, 'binary', (error) => {
518
            if(error) {
519
                fn(error)
520
            } else {
521
                fn(false)
522
            }
523
        })
524
    })
525
}
526
527
/**
528
 * Checks and removes already written ID3-Frames from a file
529
 * @param {string} filepath - Filepath to file
530
 * @param fn - (optional) Function for async usage
531
 * @returns {boolean|Error}
532
 */
533
module.exports.removeTags = function(filepath, fn) {
534
    if(isFunction(fn)) {
535
        return removeTagsAsync(filepath, fn)
536
    }
537
    return removeTagsSync(filepath)
538
}
539
540
function makePromise(fn) {
541
    return new Promise((resolve, reject) => {
542
        fn((error, result) => {
543
            if(error) {
544
                reject(error)
545
            } else {
546
                resolve(result)
547
            }
548
        })
549
    })
550
}
551
552
module.exports.Promise = {
553
    write: (tags, file) => makePromise(this.write.bind(this, tags, file)),
554
    update: (tags, file) => makePromise(this.update.bind(this, tags, file)),
555
    create: (tags) => {
556
        return new Promise((resolve) => {
557
            this.create(tags, (buffer) => {
558
                resolve(buffer)
559
            })
560
        })
561
    },
562
    read: (file, options) => makePromise(this.read.bind(this, file, options)),
563
    removeTags: (filepath) => makePromise(this.removeTags.bind(this, filepath))
564
}
565
566
module.exports.Constants = ID3Definitions.Constants
567